package org.melanesia.beans;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.regex.Pattern;

import org.melanesia.beans.exceptions.BeanAccessException;
import org.melanesia.beans.exceptions.NullCastToPrimitiveTypeException;
import org.melanesia.conf.Config;

/**
 * Utility class for accessing bean properties and manipulating property names.
 *
 * @author marcin.kielar
 */
public abstract class BeanUtils {

    /**
     * Map of wrapper classes for primitive types (primitive.class =>
     * wrapper.class).
     */
    public static final Map<Class<?>, Class<?>> WRAPPERS = new HashMap<Class<?>, Class<?>>();

    static {
        WRAPPERS.put(byte.class, Byte.class);
        WRAPPERS.put(short.class, Short.class);
        WRAPPERS.put(char.class, Character.class);
        WRAPPERS.put(int.class, Integer.class);
        WRAPPERS.put(long.class, Long.class);
        WRAPPERS.put(float.class, Float.class);
        WRAPPERS.put(double.class, Double.class);
        WRAPPERS.put(boolean.class, Boolean.class);
    }

    /** Default values for primitive types. */
    public static final Map<Class<?>, Object> DEFAULT_PRIMITIVE_VALUES = new HashMap<Class<?>, Object>();

    static {
        DEFAULT_PRIMITIVE_VALUES.put(byte.class, 0);
        DEFAULT_PRIMITIVE_VALUES.put(short.class, 0);
        DEFAULT_PRIMITIVE_VALUES.put(char.class, 0);
        DEFAULT_PRIMITIVE_VALUES.put(int.class, 0);
        DEFAULT_PRIMITIVE_VALUES.put(long.class, 0);
        DEFAULT_PRIMITIVE_VALUES.put(float.class, 0f);
        DEFAULT_PRIMITIVE_VALUES.put(double.class, 0d);
        DEFAULT_PRIMITIVE_VALUES.put(boolean.class, false);
    }

    /**
     * Returns true if <code>propertyName</code> defines a simple property.
     * Simple property is a property that identifies single field inside a
     * class, for example "value", "name", etc. Composite property is a property
     * that identifies a path to a field, for example
     * "person.address.streetName".
     *
     * @param propertyName
     *            property name to test
     * @return true if propertyName defines simple property, otherwise false
     */
    public static final boolean isSimpleProperty(final String propertyName) {
        return !propertyName.contains(Config.getCompositePropertySeparator());
    }

    /**
     * Returns first simple property part of a compound property. For example,
     * for a composite property of "person.address.streetName" this function
     * returns "person".
     *
     * @param compositePropertyName
     *            composite property name
     * @return simple property name
     */
    public static final String getSimpleProperty(final String compositePropertyName) {
        return compositePropertyName.substring(0, compositePropertyName.indexOf(Config.getCompositePropertySeparator()));
    }

    /**
     * Returns "the rest" of a composite property, ommiting first simple
     * property. For example, for a composite property of
     * "person.address.streetName", this function returns "address.streetName",
     * and for "address.streetName" returns "streetName".
     *
     * @param compositePropertyName
     *            composite property name
     * @return sub-property
     */
    public static final String getSubProperty(final String compositePropertyName) {
        return compositePropertyName.substring(getSimpleProperty(compositePropertyName).length() + 1);
    }

    /**
     * Checks wherther passed {@code str} is a camel case string.
     *
     * @param str
     *            string to test
     * @return true if string is camel case, false if it's not
     */
    public static final boolean isCamelCase(final String str) {
        if (str == null || "".equals(str)) {
            return true;
        }
        if (isSimpleProperty(str)) {
            return Pattern.matches("^[a-z]+[0-9]*([A-Z]+[a-z0-9]+)*$", str);
        } else {
            String[] strArray = split(str, Config.getCompositePropertySeparator());
            for (String astr : strArray) {
                if (!isCamelCase(astr)) {
                    return false;
                }
            }
            return true;
        }
    }

    /**
     * Splits a string on another string occurrence. This differs from
     * {@link String#split(String)}, beacause it does not do regular expressions
     * to match separator. Uses {@link StringTokenizer}.
     *
     * @param str
     *            string to split
     * @param separator
     *            separator to split on
     * @return array of strings
     */
    public static final String[] split(final String str, final String separator) {

        StringTokenizer tokenizer = new StringTokenizer(str, separator, false);
        ArrayList<String> strings = new ArrayList<String>();

        while (tokenizer.hasMoreTokens()) {
            strings.add(tokenizer.nextToken());
        }
        return strings.toArray(new String[strings.size()]);

    }

    /**
     * Strips a string from occurrences of other string.
     *
     * @param str
     *            string to remove occurrences from
     * @param stringToStrip
     *            string to remove
     * @return stripped string
     */
    public static final String strip(final String str, final String stringToStrip) {
        String[] strings = split(str, stringToStrip);

        StringBuilder builder = new StringBuilder();
        for (String s : strings) {
            builder.append(s);
        }

        return builder.toString();
    }

    /**
     * Converts names in SQL-notation (usually TEXT_TEXT_TEXT) into a javabean
     * property representation (usually textTextText). This is useful in
     * automatically determining property name, for a column value from jdbc
     * result set.
     *
     * @param str
     *            string to convert
     * @return converted string
     */
    public static final String sqlToCamelCase(final String str) {
        if (str == null) {
            return null;
        }
        if ("".equals(str)) {
            return str;
        }
        if (isCamelCase(str)) {
            return str;
        }
        if (str.length() == 1) {
            return str.toLowerCase(Locale.getDefault());
        }

        StringBuilder b = new StringBuilder();

        if (isSimpleProperty(str)) {
            String[] tokens = split(str, "_");
            for (String t : tokens) {
                if (t.length() == 0) {
                    continue;
                }
                b.append(t.substring(0, 1).toUpperCase());
                b.append(t.substring(1).toLowerCase());
            }

        } else {
            String[] strArray = split(str, Config.getCompositePropertySeparator());
            for (String astr : strArray) {

                if (b.length() > 0) {
                    b.append(Config.getCompositePropertySeparator());
                }

                if (isCamelCase(astr)) {
                    b.append(astr);
                } else {
                    b.append(sqlToCamelCase(astr));
                }
            }
        }

        String ret = b.toString();
        return ret.substring(0, 1).toLowerCase(Locale.getDefault()) + ret.substring(1);
    }

    /**
     * Capitalizes string. This method changes first character of the string
     * <code>str</code> to uppercase using default locale. If the
     * <code>str</code> is null or an empty string, it just returns
     * <code>str</code>.
     *
     * @param str
     *            string to capitalize
     * @return capitalized string
     */
    public static final String capitalize(final String str) {
        if (str == null) {
            return null;
        }
        if ("".equals(str)) {
            return str;
        }
        if (str.length() == 1) {
            return str.toUpperCase(Locale.getDefault());
        }

        return str.substring(0, 1).toUpperCase(Locale.getDefault()) + str.substring(1);
    }

    /**
     * Decapitalizes string. This method changes first character of the string
     * <code>str</code> to lowercase using default locale. If the
     * <code>str</code> is null or an empty string, it just returns
     * <code>str</code>.
     *
     * @param str
     *            string to decapitalize
     * @return decapitalized string
     */
    public static final String decapitalize(final String str) {
        if (str == null) {
            return null;
        }
        if ("".equals(str)) {
            return str;
        }
        if (str.length() == 1) {
            return str.toLowerCase(Locale.getDefault());
        }

        return str.substring(0, 1).toLowerCase(Locale.getDefault()) + str.substring(1);
    }

    /**
     * Sets the method to accessible / unaccessible depending on the
     * <code>accessible</code> value.
     *
     * @param method
     *            method to change accessibility of
     * @param accessible
     *            accessibility flag
     */
    public static final void setAccessible(final Method method, final boolean accessible) {
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            @Override
            public Void run() {
                method.setAccessible(accessible);
                return null;
            }
        });
    }

    /**
     * Sets the field to accessible / unaccessible depending on the
     * <code>accessible</code> value.
     *
     * @param field
     *            field to change accessibility of
     * @param accessible
     *            accessibility flag
     */
    public static final void setAccessible(final Field field, final boolean accessible) {
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            @Override
            public Void run() {
                field.setAccessible(accessible);
                return null;
            }
        });
    }

    /**
     * Returns a wrapper class for given primitive class. Eg. returns Long.class
     * for long.class.
     *
     * @param primitiveClass
     *            primitive class
     * @return wrapper class
     */
    public static final Class<?> getWrapperClass(final Class<?> primitiveClass) {
        return WRAPPERS.get(primitiveClass);
    }

    /**
     * Returns primitive class for given wrapper class. Eg. returns long.class
     * for Long.class
     *
     * @param wrapperClass
     *            wrapper class
     * @return primitive class
     */
    public static final Class<?> getPrimitiveClass(final Class<?> wrapperClass) {
        for (Map.Entry<Class<?>, Class<?>> entry : WRAPPERS.entrySet()) {
            if (entry.getValue().equals(wrapperClass)) {
                return entry.getKey();
            }
        }

        return null;
    }

    /**
     * Smart, autoboxing-aware isAssignableFrom. Returns true when: - class1 is
     * assignable from class2 as described in
     * {@link Class#isAssignableFrom(Class)} - class1 is a wrapper / primitive
     * equivalent class for class2
     *
     * @param class1
     *            first class
     * @param class2
     *            second class
     * @return true if class1 is assignable from class 2, otherwise false
     */
    public static final boolean isAssignableFrom(final Class<?> class1, final Class<?> class2) {

        // First, check if class1 is assignable from class2
        if (class1.isAssignableFrom(class2)) {
            return true;
        } else {

            // If not, there is still posibility, that we have an autoboxing
            // issue,
            // where for example int.class is not assignable from Integer.class
            // and vice versa.
            for (Map.Entry<Class<?>, Class<?>> entry : WRAPPERS.entrySet()) {
                if ((entry.getKey().isAssignableFrom(class1) && entry.getValue().isAssignableFrom(class2))
                        || (entry.getKey().isAssignableFrom(class2) && entry.getValue().isAssignableFrom(class1))) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Smart autoboxing-aware cast. If value is of primitive or wrapper type of
     * the given class, then just returns value without actual casting. This way
     * lets JVM handle auto(un)boxing.
     *
     * @param value
     *            value to cast
     * @param toClass
     *            class to cast the value to
     * @return value after the cast
     * @throws NullCastToPrimitiveTypeException
     *             when null value is being casted to a primitive type
     *             (int.class, boolean.class, etc.)
     *
     */
    public static final Object cast(final Object value, final Class<?> toClass) throws NullCastToPrimitiveTypeException {
        if (value == null) {

            if (isPrimitiveClass(toClass)) {
                // This was a smart hack indeed, but honestly, too dangerous.
                // "Zeroes" and "falses" appearing out of nowhere in
                // primitive-type properties when database returns nulls is a
                // bad, bad idea.
                // Instead we just throw NullCastToPrimitiveTypeException
                //
                // return DEFAULT_PRIMITIVE_VALUES.get(toClass);

                throw new NullCastToPrimitiveTypeException(toClass);
            } else {
                return null;
            }
        } else {
            if (isWrapper(toClass, value.getClass()) || isPrimitive(toClass, value.getClass())) {
                return value;
            } else {
                return toClass.cast(value);
            }
        }
    }

    /**
     * Determines if <code>clazz</code> is a wrapper class (Integer, Boolean,
     * etc.) for some primitive type. (int, boolean, etc.)
     *
     * @param clazz
     *            class to test
     * @return true if <code>clazz</code> is a wrapper class
     */
    public static final boolean isWrapperClass(final Class<?> clazz) {
        return WRAPPERS.containsValue(clazz);
    }

    /**
     * Determines if <code>clazz</code> is a primitive type. (like int, long,
     * boolean, etc.)
     *
     * @param clazz
     *            class to test
     * @return true if <code>clazz</code> is a primitive type
     */
    public static final boolean isPrimitiveClass(final Class<?> clazz) {
        return WRAPPERS.containsKey(clazz);
    }

    /**
     * Determines id class1 is a primitive type of wrapper class2. This method
     * calls <code>isWrapper(class2, class1)</code>, as this will tell if class2
     * is a wrapper for primitive type class1.
     *
     * @param class1
     *            primitive type
     * @param class2
     *            wrapper class
     * @return true, if class1 is a primitive type for wrapper class2
     */
    public static final boolean isPrimitive(final Class<?> class1, final Class<?> class2) {
        // class1 is a primitive type of class2 then and only then,
        // when class2 is a wrapper of class1
        return isWrapper(class2, class1);
    }

    /**
     * Determines if class1 is a wrapper class, for primitive type specified by
     * class2.
     *
     * @param class1
     *            wrapper class
     * @param class2
     *            primitive type
     * @return true if class1 is a wrapper class for primitive class2
     */
    public static final boolean isWrapper(final Class<?> class1, final Class<?> class2) {
        return class1.equals(WRAPPERS.get(class2));
    }

    /**
     * Returns first not null value.
     *
     * @param <T>
     *            return type
     * @param values
     *            array of values
     * @return first not null value
     */
    public static final <T> T nvl(final T... values) {
        for (T value : values) {
            if (value != null) {
                return value;
            }
        }

        return null;
    }

    /**
     * Creates instance of class. This is different from
     * {@link Class#newInstance()} in that it is able to construct private
     * nested classes, as long as they are declared {@code static}. The
     * instantiation is done using parameterless constructor, and if necessary,
     * using {#link Constructor#setAccessible}.
     *
     * @param <T>
     *            return type
     * @param clazz
     *            class to instantiate
     * @return new instance of class
     */
    public static final <T> T newInstance(final Class<T> clazz) {
        try {
            Constructor<T> constructor = clazz.getDeclaredConstructor();
            constructor.setAccessible(true);

            return constructor.newInstance();
        } catch (Throwable t) {
            throw new BeanAccessException(t);
        }
    }
}
